[iOS] SwiftでProtocol Buffersを使ってみたい
こんにちは。きんくまです。
今回はProtocol BuffersをSwiftでも使ってみたい!です。
Protocol Buffersとは?
よく知らなかったんですが、いろいろと調べてみるとProtocol Buffersはざっくりこんな感じ
- Googleが使っているデータをやりとりするときのデータ構造を表す(XMLとかJSONとかみたいにデータ構造を表すことが可能)
- gRPCとセットで使われることが多い
- バイナリ形式で通信できるので、文字列形式のJSONと比べてデータ量が少なくて済む
手順の概要
準備編
1. コマンド用のProtocol Buffersをインストール
2. Swift Protobufからprotoc-gen-swiftプラグインをインストール
使用編
1. terminal上でprotoコマンドで定義ファイルの.protoをコンパイル
2. 1で生成された.pb.swiftファイルをXcodeのプロジェクトに追加
3. XcodeにSwift Protobufをライブラリとして追加
4. 使用する
使ってみよう!準備編
1. コマンド用のProtocol Buffersをインストール
2022/08/17現在最新版はv21.5
私の環境はM1 Macなので、armのバイナリを落としてきました
- protoc-21.5-osx-aarch_64.zip
もしIntel Macだったら、protoc-21.5-osx-x86_64.zip を落とせばよいと思います。
面倒だったら protoc-21.5-osx-universal_binary.zip がどちらのMacでも使えると思うのでそれにすれば良さそう。
パスを通すのですが、次のステップでまとめてやります。
2. Swift Protobufからprotoc-gen-swiftプラグインをインストール
apple / swift-protobuf(GitHub。Apple公式のリポジトリがあるんですね!)
上記リポジトリのREADMEを読むと以下のようにすれば良いみたい
git clone https://github.com/apple/swift-protobuf.git cd swift-protobuf swift build -c release
3行目でコンパイルしています。うまくいくと以下のディレクトリが作成されます。
.build/release
この中に protoc-gen-swift というバイナリができていればOK
パスを通します。下のサンプルはシェルにzshを使った場合なので、他の環境の場合は適宜読み替えてください
vi ~/.zshrc
どこかに以下を記述
# Protocol Buffers export PATH="/パスを記述/protoc-21.5-osx-aarch_64/bin:$PATH" export PATH="/パスを記述/.build/arm64-apple-macosx/release:$PATH"
上の行は 手順1のprotocが入っているbinディレクトリまでのパス
です 。
下の行は 手順2のprotoc-gen-swiftが入っているreleaseディレクトリまでのパス
です。
保存したら設定ファイルを読み込み直します
source ~/.zshrc
以下のコマンドでパスが通っているか確認
which protoc
もういっこ
which protoc-gen-swift
パスがうまく通っていれば、not foundにならずコマンドのバイナリが置いてある場所が表示されます
使ってみよう!使用編
1. terminal上でprotoコマンドで定義ファイルの.protoをコンパイル
定義ファイルを作ります。定義ファイルは.protoファイルになります。
BookInfo.proto
syntax = "proto3"; message BookInfo { int64 id = 1; string title = 2; string author = 3; }
上記.protoファイルが置いてあるディレクトリにcdで移動してからコンパイルします。
protoc --swift_out=. BookInfo.proto
生成されたファイル。このファイル自体は自動生成されるのであんまり見る必要はないと思います。
BookInfo.pb.swift
// DO NOT EDIT. // swift-format-ignore-file // // Generated by the Swift generator plugin for the protocol buffer compiler. // Source: BookInfo.proto // // For information on using the generated types, please see the documentation: // https://github.com/apple/swift-protobuf/ import Foundation import SwiftProtobuf // If the compiler emits an error on this type, it is because this file // was generated by a version of the `protoc` Swift plug-in that is // incompatible with the version of SwiftProtobuf to which you are linking. // Please ensure that you are building against the same version of the API // that was used to generate this file. fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck { struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {} typealias Version = _2 } struct BookInfo { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. var id: Int64 = 0 var title: String = String() var author: String = String() var unknownFields = SwiftProtobuf.UnknownStorage() init() {} } // MARK: - Code below here is support for the SwiftProtobuf runtime. extension BookInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = "BookInfo" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "id"), 2: .same(proto: "title"), 3: .same(proto: "author"), ] mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { switch fieldNumber { case 1: try decoder.decodeSingularInt64Field(value: &self.id) case 2: try decoder.decodeSingularStringField(value: &self.title) case 3: try decoder.decodeSingularStringField(value: &self.author) default: break } } } func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws { if self.id != 0 { try visitor.visitSingularInt64Field(value: self.id, fieldNumber: 1) } if !self.title.isEmpty { try visitor.visitSingularStringField(value: self.title, fieldNumber: 2) } if !self.author.isEmpty { try visitor.visitSingularStringField(value: self.author, fieldNumber: 3) } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: BookInfo, rhs: BookInfo) -> Bool { if lhs.id != rhs.id {return false} if lhs.title != rhs.title {return false} if lhs.author != rhs.author {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } }
2. 1で生成された.pb.swiftファイルをXcodeのプロジェクトに追加
Xcodeのプロジェクトに BookInfo.pb.swift を追加します
3. XcodeにSwift Protobufをライブラリとして追加
Xcode 13.1の場合
File > Add Packages
検索欄に protobuf
と入れると SwiftProtobuf が表示される
SwiftProtobufだけチェックを入れます。(ちなみに他にチェックを入れると他はコマンドライン用のものなのでコンパイルエラーになります。ハマったw)
4. 使用する
使ってみます
func sample() { var info = BookInfo() info.id = 1734 info.title = "何かすごいことが書いてある本 a book with something amazing" info.author = "田中太郎" if let binData = try? info.serializedData() { print("data \(binData)") } if let jsonStrData = try? info.jsonUTF8Data(), let jsonStr = String(data: jsonStrData, encoding: .utf8) { print("str \(jsonStr)") print("jsondata \(jsonStrData)") } }
出力です
data 91 bytes str {"id":"1734","title":"何かすごいことが書いてある本 a book with something amazing","author":"田中太郎"} jsondata 120 bytes
JSON文字列でも出力できるので、比べてみました。通常のバイナリにエンコードした場合は91バイトですが、JSONにエンコードした場合は120バイトでした。
たしかにJSONよりはデータサイズが小さくなっていました。
感想
Protocol Buffers自体はWikiによると14年も前からあるものでした
JSONと違って、型が定義できたりoptionalに対応しているのが良さそうでした。
一方フィールドの番号決めをしっかりやらないと、のちのちに大変になりそうだなという感じ。
ふだんはREST API + JSON形式 でサーバーとやりとりしているので、gRPC + Protocol Buffersというものがあることが知れて良かったです。